Описание проекта¶

Проанализируйте ассортимент товаров на основе транзакций интернет-магазина товаров для дома и быта:

  • Проведите исследовательский анализ данных;
  • Проанализируйте торговый ассортимент;
  • Сформулируйте рекомендации для менеджмента магазина.

1. Изучение общей информации¶

Импорт библиотек¶

In [1]:
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import plotly.express as px
import scipy.stats as stats

Загрузка файла¶

In [2]:
# загружаем датасет из файла
df = pd.read_csv('', sep=',')

Изучение содержания¶

In [3]:
# выводим пять первых строк датасета
df.head()
Out[3]:
date customer_id order_id product quantity price
0 2018100100 ee47d746-6d2f-4d3c-9622-c31412542920 68477 Комнатное растение в горшке Алое Вера, d12, h30 1 142.0
1 2018100100 ee47d746-6d2f-4d3c-9622-c31412542920 68477 Комнатное растение в горшке Кофе Арабика, d12,... 1 194.0
2 2018100100 ee47d746-6d2f-4d3c-9622-c31412542920 68477 Радермахера d-12 см h-20 см 1 112.0
3 2018100100 ee47d746-6d2f-4d3c-9622-c31412542920 68477 Хризолидокарпус Лутесценс d-9 см 1 179.0
4 2018100100 ee47d746-6d2f-4d3c-9622-c31412542920 68477 Циперус Зумула d-12 см h-25 см 1 112.0

Видим, что:

  1. Названия столбцов адекватные - изменять их не нужно.
  2. Формат даты неудобный: дата и час слились воедино - нужно привести к формату datetime
  3. Номер покупателя очень длинный - по возможности укоротить
  4. Наименование товара полное - сделать столбец с укороченнным, чтобы легче было распределять товары по категориям.
In [4]:
# посмотрим форматы даных
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6737 entries, 0 to 6736
Data columns (total 6 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   date         6737 non-null   int64  
 1   customer_id  6737 non-null   object 
 2   order_id     6737 non-null   int64  
 3   product      6737 non-null   object 
 4   quantity     6737 non-null   int64  
 5   price        6737 non-null   float64
dtypes: float64(1), int64(3), object(2)
memory usage: 315.9+ KB

Видим, что:

  1. Неправильный-таки формат даты
  2. Покупателей и товары нужно перевести к типу string

Преобразование типов данных¶

In [5]:
# преобразуем имена и названия к типу string, чтобы мы могли изменять значения и анализировать их
df['customer_id'] = df['customer_id'].astype('string')
df['product'] = df['product'].astype('string')

# преобразуем содержание временных столбцов к типу datetime
df['date'] = df['date'].astype('string')
df['date'] = pd.to_datetime(df['date'], format='%Y%m%d%H')
In [6]:
# проверим, поменялись ли форматы данных
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6737 entries, 0 to 6736
Data columns (total 6 columns):
 #   Column       Non-Null Count  Dtype         
---  ------       --------------  -----         
 0   date         6737 non-null   datetime64[ns]
 1   customer_id  6737 non-null   string        
 2   order_id     6737 non-null   int64         
 3   product      6737 non-null   string        
 4   quantity     6737 non-null   int64         
 5   price        6737 non-null   float64       
dtypes: datetime64[ns](1), float64(1), int64(2), string(2)
memory usage: 315.9 KB

Определение временного интервала датасета¶

In [7]:
# посмотрим распределение событий по датам
df['date'].hist(bins=150);

# называем оси и заголовок
plt.xlabel('Время')
plt.ylabel('Количество')
plt.title('Распределение событий по датам')
plt.show()
In [8]:
# находим минимальную и максимальную даты в датасете
display(df['date'].min())
display(df['date'].max())

print("Данные охватывают", df['date'].max() - df['date'].min())
Timestamp('2018-10-01 00:00:00')
Timestamp('2019-10-31 16:00:00')
Данные охватывают 395 days 16:00:00

Видим, что:

  1. Датасет охватывает период с октября 2018 по октябрь 2019 включительно, или 395 дней
  2. Больше всего событий произошло в мае-июне 2019 года, причём такая интенсивность событий может указывать на наличие дубликатов, аномалий либо технических ошибок.
  3. Меньше всего событий наблюдается в январе, а также последних месяцах 2019 года. Возможно, магазин постепенно теряет покупателей: если это предположение верно, то мы увидим также снижение выручки и/или объёма закупок в эти месяцы.
In [9]:
# посмотрим распределение количества товаров
df['quantity'].hist(bins=100);

# называем оси и заголовок
plt.xlabel('Количество товаров')
plt.ylabel('Частота')
plt.title('Распределение количества товаров')
plt.show()
In [10]:
# проверим адекватность айди заказов
display(df['order_id'].astype('int').hist())
<AxesSubplot:>

Видим, что:

В основном покупатели берут штучный товар, т.е. это розничный магазин - из дальнейшего анализа необходимо будет исключить оптовые закупки, если таковые будут.

Номера заказов расположены не последовательно, поэтому стоит задать вопрос специалистам техподдержки магазина, по какой причине нумерация прерывается.

In [11]:
# посмотрим распределение цены товаров
df['price'].hist(bins=100);

# называем оси и заголовок
plt.xlabel('Цена')
plt.ylabel('Частота')
plt.title('Распределение цены товаров')
plt.show()

Видим, что:

Цена покупаемого товара в основном не превышает 1000 у.е.

2. Обработка данных¶

Добавление столбцов¶

In [12]:
# добавим столбцы с указанием года, месяца и недели в датасет
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['week'] = df['date'].dt.isocalendar().week

# добавим столбцы с указанием дня недели и часа
df['dow'] = df['date'].dt.dayofweek + 1
df['hour'] = df['date'].dt.hour

# также добавим столбец с сочетанием года и месяца
df['ymonth'] = df['year'].astype('string') + df['month'].astype('string')
df['ymonth'] = pd.to_datetime(df['ymonth'], format='%Y%m')
In [13]:
# добавим столбец с суммой выручки за товар как произведения цены и количества
df['revenue'] = df['price'] * df['quantity']
In [14]:
# попробуем добавить столбец с коротким ID покупателей, ограничив его шестью последними знаками

# сколько уникальных покупателей в датасете?
display(df['customer_id'].nunique())
2451
In [15]:
# добавляем столбец
df['cid'] = [x.strip()[-7:] for x in df['customer_id']]

# проверяем, не склеились ли ID
display(df['cid'].nunique())
2451
In [16]:
# добавим столбец с двумя первыми словами из наименования товаров
df['prod'] = [x.split()[0] + ' ' + x.split()[1] for x in df['product']]

Обработка явных дубликатов¶

In [17]:
# выводим количество пропусков в каждом столбце датасета
df.isna().sum() 
Out[17]:
date           0
customer_id    0
order_id       0
product        0
quantity       0
price          0
year           0
month          0
week           0
dow            0
hour           0
ymonth         0
revenue        0
cid            0
prod           0
dtype: int64
In [18]:
# выводим количество полных дубликатов в датасете
df.duplicated().sum()
Out[18]:
0

Вывод:

Полных дубликатов не обнаружено

Обработка неявных дубликатов¶

In [19]:
# посмотрим, сколько в нашем датасете дубликатов по основным колонкам, за исключением колонки со временем заказа
# наличие таких дубликатов свидетельствует о сбоях в системе заказов либо об автоматических заказах

display(df.drop_duplicates(subset=['customer_id', 'order_id', 'product', 'quantity', 'price'])['date'].count() / df['date'].count())
0.7233189847112959

Такие заказы, составляющие почти 28% датасета, нужно исключить из анализа

In [20]:
# удаляем их
df = df.drop_duplicates(subset=['customer_id', 'order_id', 'product', 'quantity', 'price'])
In [21]:
# далее проверим адекватность цен на товары стоимостью более 2000 у.е.
# не было ли ошибок при вводе этих цен

display(df.query('price >= 2000')
        .pivot_table(index='prod', values='price', aggfunc='max'))
price
prod
Tepмокружка AVEX 2399.0
Автоматическая щетка 7229.0
Афеляндра скуарроса 3524.0
Бак для 3749.0
Ведро для 3749.0
Весы напольные 2849.0
Гладильная доска 7424.0
Гладильная доска-стремянка 2399.0
Гортензия Микс 3599.0
Двуспальное постельное 2024.0
Доска гладильная 3299.0
Ерш для 3524.0
Карниз алюминиевый 2099.0
Коврик для 5474.0
Коврик придверный 2009.0
Комплект для 5399.0
Котел алюминиевый 2924.0
Мантоварка-пароварка WEBBER 2219.0
Мусорный контейнер 5512.0
Набор Vileda 2924.0
Набор инструментов 5399.0
Наматрасник Wellness 3074.0
Новогоднее дерево 3524.0
Одеяло Wellness 4724.0
Покрывало жаккард 6134.0
Полки QWERTY 4312.0
Пылесос DELTA 2249.0
Сиденье для 6149.0
Скатерть 350х150 2249.0
Скатерть Арлет 2174.0
Скатерть Джулия 2249.0
Стремянка 5 3974.0
Стремянка 7 7724.0
Стремянка COLOMBO 3449.0
Стремянка Colombo 2699.0
Стремянка FRAMAR 4499.0
Стремянка Scab 5549.0
Стремянка алюминиевая 4949.0
Стремянка-табурет алюминиевая 2699.0
Стремянки Colombo 3974.0
Сумка-тележка 2-х 2849.0
Сумка-тележка 3-х 2699.0
Сумка-тележка TWIN 2624.0
Сумка-тележка хозяйственная 8737.0
Сушилка Meliconi 5594.0
Сушилка для 7004.0
Сушилка уличная 14917.0
Урна уличная 7349.0
Урна-пепельница из 5287.0
Фал капроновый 2099.0
Цитрофортунелла Кумкват 3074.0
Швабра для 2624.0
Швабра хозяйственная 3224.0
Штора для 4424.0

Мы проверили цены указанных выше товаров в интернете и нашли, что они значительно не отличаются от представленных в датасете

In [22]:
# далее проверим, нет ли заказов, приписываемых сразу нескольким покупателям

bugged_orders = (
        df.pivot_table(index='order_id', values='customer_id', aggfunc='nunique') # таблица "заказ" - "уник кол-во покупателей"
          .reset_index()
          .sort_values('customer_id', ascending=False)
          .query('customer_id > 1')['order_id'] # если заказ сделан более чем одним покупателем, то это ошибка
          .to_list()
                )
display(bugged_orders)
[72845,
 71480,
 69485,
 69310,
 69833,
 72790,
 72778,
 14872,
 71542,
 71054,
 71663,
 70726,
 69531,
 70542,
 70903,
 69283,
 71226,
 71571,
 69410,
 69345,
 70808,
 70114,
 70631,
 71461,
 72950,
 71648,
 70946,
 68785,
 72188]
In [23]:
# какой процент данных занимают эти записи?

display(df.query('order_id in @bugged_orders')['order_id'].count() / df['order_id'].count())
0.013544018058690745

1,3%. Удаляем

In [24]:
# удаляем

df = df.query('order_id not in @bugged_orders')

Вывод:

Мы провели предобработку данных, очистив датасет от возможных ошибок и дубликатов

Привели данные к удобным типам и добавили столбцы, которые помогут нам в дальнейшем анализе

3. Анализ данных¶

Поиск и удаление аномалий¶

Сперва посмотрим распределение кол-ва покупаемого товара, чтобы исключить из анализа оптовые закупки

In [25]:
# построим график размаха продаж
sns.boxplot(x=df['quantity'], orient='h')

# называем график и его оси
plt.xlabel('Количество товара, шт')
plt.title('Диаграмма размаха количества товара')
plt.show()


# построим дополнительный детальный график, ограничив диапазон кол-ва предыдущего до 200 шт
sns.boxplot(x=df['quantity'], orient='h')

# ограничиваем ось Х
plt.axis([0, 200, None, None])

# называем график и его оси
plt.xlabel('Количество товара, шт')
plt.title('Детальный размах количества товара')
plt.show()
In [26]:
# посмотрим, какие товары покупают оптом
display(df.query('quantity > 50')
              .pivot_table(index='prod', values='quantity', aggfunc='sum')
              .reset_index()
              .sort_values('quantity', ascending=False)
              .head(15)
              .plot(x='prod', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Товар')
plt.ylabel('Количество в шт')
plt.title('Оптом у магазина покупают следующие товары')
plt.show()
<AxesSubplot:xlabel='prod'>

Видим, что:

В датасете есть много оптовых закупок. Принимая во внимание, что ассортимент магазина включает немало мелких и/или парных предметов (вилки, рассада, крючки и пр.), будем считать оптовыми такие операции, в которых закупается более чем 50 экземпляров товара

In [27]:
# смотрим, какую долю составляют операции к удалению
display(len(df.query('quantity > 50')) / len(df))

# сколько выручки приносят оптовые закупки?
display(df.query('quantity > 50')['revenue'].sum())
0.004784688995215311
880158.0
In [28]:
# также проверим, что наибольшую выручку приносят именно операции продажи товара в одном экземпляре
# (в одном заказе может быть несколько операций)

revenue_top_quantity = (df.pivot_table(index='quantity', values='revenue', aggfunc='sum')
                      .reset_index()
                      .sort_values('revenue', ascending=False) 
                     ) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек

revenue_top_quantity.head()
Out[28]:
quantity revenue
0 1 2258065.0
48 1000 675000.0
1 2 228022.0
9 10 107450.0
2 3 86046.0
In [29]:
# Посмотрим на динамику оптовых продаж, прежде чем исключить их
revenue_by_month = (df.query('quantity > 50')
                      .pivot_table(index='ymonth', values='revenue', aggfunc='sum')
                      .reset_index()
                      .sort_values('ymonth', ascending=True) 
                     ) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек

revenue_by_month.plot(x='ymonth', y='revenue', kind='bar') # строим диаграмму

# называем оси и заголовок
plt.xlabel('Месяц')
plt.ylabel('Выручка в у.е.')
plt.title('Распределение оптовой выручки по месяцам')
plt.show()

Итак, оптовые покупки составляют 0,5% от объёма данных. Из таблицы и помесячного графика выручки мы видим, что 675 тыс у.е. оптовой выручки приходится на одну аномальную операцию, совершённую в июне 2019 года. Исключив её, а также операции покупки более 50 экземпляров товара, мы получим адекватные данные о розничных закупках

In [30]:
# исключаем оптовые закупки, которые составляют 0,5% операций,
# в том числе разовый заказ на 675 тыс у.е.
df = df.query('quantity <= 50')
In [31]:
# посмотрим распределение цен проданных товаров - построим график размаха
sns.boxplot(x=df['price'], orient='h')

# называем график и его оси
plt.xlabel('Цена, у.е.')
plt.title('Диаграмма размаха цен')
plt.show()


# построим дополнительный детальный график, ограничив диапазон цены предыдущего до 2 тыс у.е.
sns.boxplot(x=df['price'], orient='h')

# ограничиваем ось Х
plt.axis([0, 2000, None, None])

# называем график и его оси
plt.xlabel('Цена, у.е.')
plt.title('Детальный размах цен')
plt.show()

Видим, что:

Основная часть товаров продаётся по цене от 50 до 500 у.е. за штуку. Аномально высокая цена товара начинается уже на уровне 1 000 у.е.

Однако ранее мы уже проанализировали цены выше 2 000 у.е. и заключили, что ошибок ввода или подозрительно высоких цен в них нет.

Поэтому мы не исключаем из анализа дополнительные данные

На этом этапе мы исключили из анализа оптовые закупки

Детальный анализ¶

Общая информация¶

In [32]:
# посмотрим, сколько покупателей и заказов есть в логах
display(df['customer_id'].nunique())
display(df['order_id'].nunique())

# сколько заказов приходится в среднем на покупателя?
display(df['order_id'].nunique() / df['customer_id'].nunique())
2375
2734
1.1511578947368422
In [33]:
# посмотрим, сколько выручки получила компания за период
display(df['revenue'].sum())

# сколько выручки приходится в среднем на покупателя?
display(df['revenue'].sum() / df['customer_id'].nunique())
3214457.0
1353.4555789473684
In [34]:
# посмотрим, сколько товаров составляют ассортимент магазина
display(df['product'].nunique())

# сколько товаров приходится в среднем на заказ?
display(df['product'].count() / df['order_id'].nunique())
2318
1.7498171177761521

Выводы:

  1. За рассматриваемый период магазин продал 2318 уникальный товар. Менеджеры могут совоставить это количество с общим числом номенклатурных единиц и оценить объём товаров, которые в этом периоде не продавались вовсе.
  2. Через магазин прошло 2375 клиентов, которые совершили 2734 заказа. Соответственно, костяк клиентов за период совершил лишь один заказ.
  3. Розничная выручка за период составила 3,2 млн у.е., при этом один покупатель в среднем принёс 1 353 у.е. выручки

Категоризация товаров¶

In [35]:
# добавим файл с проставленными нами парами "товар"-"категория" отдельным датасетом
df_cat = pd.read_csv('https://downloader.disk.yandex.ru/disk/3040a2d8dc83a71b1a4e37c11a64a0d6d2f07580785bb39a6e564b8f0751c2f6/64960832/Kxhhxzo7VermcTtygkQTfWBQydlZ4zbI96VGP8F9-nCStzqYYa-ZdbhSmF2fovL2NA9CJ-2A5sinEp9530_-oA%3D%3D?uid=0&filename=cat1.csv&disposition=attachment&hash=XzzcjF4wOn/8Y0L%2BtDzQtJx9gItFqHPqpUukXHSsXQRfJL6sf6H754dubHvwfObqq/J6bpmRyOJonT3VoXnDag%3D%3D&limit=0&content_type=text%2Fplain&owner_uid=34392834&fsize=277393&hid=430a5ebe6a78b848daf71b414cc1a8fd&media_type=spreadsheet&tknv=v2', sep=',')
In [36]:
# выведем его на экран
df_cat.head()
Out[36]:
product cat
0 Комнатное растение в горшке Алое Вера, d12, h30 растения
1 Комнатное растение в горшке Кофе Арабика, d12,... растения
2 Радермахера d-12 см h-20 см растения
3 Хризолидокарпус Лутесценс d-9 см растения
4 Циперус Зумула d-12 см h-25 см растения
In [37]:
# добавим в основной датасет столбец с категорией товара, притянув её по имени товара
df = pd.merge(df, df_cat, on='product')

Проведём детальный анализ данных, чтобы определить тренды и сформулировать рекомендации для менеджеров магазина

In [38]:
# Построим столбчатую диаграмму распределения выручки и продаж по месяцам
dbm = df.groupby(['ymonth'])[['revenue','quantity']].sum() # cводная таблица с нужными цифрами


fig = plt.figure() # создаём график

ax = fig.add_subplot(111) # добавляем оси
ax2 = ax.twinx() # дублируем созданную ось

width = 0.3

# строим два графика
dbm.revenue.plot(kind='bar', color='magenta', ax=ax, width=width, position=1, label='Выручка')
dbm.quantity.plot(kind='bar', color='cyan', ax=ax2, width=width, position=0, label='Продажи')

# добавляем названия
plt.legend(loc="upper right")
ax.set_ylabel('Выручка в у.е.')
ax2.set_ylabel('Продажи в шт')
plt.title('Распределение продаж по месяцам в у.е. и шт')

plt.show()

Итак, мы анализируем оставшиеся продажи на 2,8 млн у.е., очищенные от оптовых и аномальных операций.

По распределению продаж видно, что пиковые продажи наблюдались осенью 2018 и постепенно снижались: к концу анализируемого периода продажи упали в полтора раза, а в октябре 2019 года продажи были почти вдвое меньше октября 2018-го.

При этом в январе и июне наблюдались спады продаж. Январский спад возможен по причине длительных праздников, а июньский, например, сезоном отпусков.

Распределение объёмов закупок товара в основном повторяет распределение выручки, за исключением двух моментов:

  1. Пик здесь наблюдается в апреле-мае. Вероятно, в эти месяцы покупают много недорогих товаров
  2. Нет проседания продаж в июне

Посмотрим, как распределена выручка по покупателям и заказам.

In [ ]:
 
In [39]:
# Построим столбчатую диаграмму распределения выручки и продаж по топ-10 покупателей
dbc = df.groupby(['cid'])[['revenue','quantity']].sum().sort_values('quantity', ascending=False).head(10)

fig = plt.figure() # создаём график

ax = fig.add_subplot(111) # добавляем оси
ax2 = ax.twinx() # дублируем созданную ось

width = 0.3

# строим два графика
dbc.revenue.plot(kind='bar', color='magenta', ax=ax, width=width, position=1, label='Выручка')
dbc.quantity.plot(kind='bar', color='cyan', ax=ax2, width=width, position=0, label='Продажи')

# добавляем названия
plt.legend(loc="upper right")
ax.set_ylabel('Выручка в у.е.')
ax2.set_ylabel('Продажи в шт')
plt.title('Распределение продаж по топ-10 покупателям в у.е. и шт')

plt.show()

Видим, что на двух самых крупных покупателей приходится 7% всех продаж, но уже на следующих менее 2%. Они же крупнейшие покупатели по количеству товаров.

Посмотрим, как часто они совершали покупки

In [40]:
# Построим диаграммы распределения выручки по месяцам по каждому из этих покупателей
display(df.query('cid in ["ee86d3b","e40d7db"]')
              .pivot_table(index='ymonth', values='quantity', aggfunc='sum')
              .reset_index()
              .sort_values('ymonth', ascending=True)
              .plot(x='ymonth', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Месяц')
plt.ylabel('Товары в шт')
plt.title('Закупки покупателей ee86d3b и e40d7db')
plt.show()
<AxesSubplot:xlabel='ymonth'>

Итак, они совершали покупки вплоть до марта 2019 года, но не далее. Вероятно, это упущенные клиенты

А как распределены товары по заказам?

In [41]:
# Построим столбчатую диаграмму распределения товаров по заказам
display(df.pivot_table(index='order_id', values='quantity', aggfunc='sum')
                      .reset_index()
                      .sort_values('quantity', ascending=False)
                      .head(10)
                      .plot(x='order_id', y='quantity', kind='bar')
                     ) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек


# называем оси и заголовок
plt.xlabel('Заказ')
plt.ylabel('Количество товара, шт')
plt.title('Распределение товаров по топ-10 заказов')
plt.show()
<AxesSubplot:xlabel='order_id'>

Поскольку мы исключили оптовые заказа, колебание кол-ва товаров в оставшихся заказов несильное.

Перейдём к анализу популярных товаров

In [42]:
# Построим столбчатую диаграмму продаж топ-10 популярных товаров
display(df.pivot_table(index='prod', values='quantity', aggfunc='sum')
                      .reset_index()
                      .sort_values('quantity', ascending=False)
                      .head(10)
                      .plot(x='prod', y='quantity', kind='bar')
                     ) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек


# называем оси и заголовок
plt.xlabel('Название товара (укор.)')
plt.ylabel('Количество товара, шт')
plt.title('Топ-10 популярных товаров')
plt.show()
<AxesSubplot:xlabel='prod'>

Из графика выше можно заключить, что больше всего магазин продаёт растения и цветы, в том числе искусственные.

Посмотрим, какая категория товаров пользуется наибольшим спросом.

Для этого мы присвоили каждому из товаров одну из шести следующих категорий: одежда и обувь, растения, дом и быт, кухонные принадлежности, хранение и перевозка, прочее.

Анализ по категориям¶

In [43]:
# Построим диаграммы распределения выручки и количества по категориям товаров
plotpie = (df.pivot_table(index='cat', values=['revenue','quantity'], aggfunc='sum')
                      .reset_index()
                      .sort_values('revenue', ascending=False) 
                     ) # cводная таблица с нужными цифрами, отсортированная по убыванию кол-ва точек


# размещаем две диаграммы рядом друг с другом
fig, (ax1,ax2) = plt.subplots(1,2,figsize=(12,12)) 

# данные и описания для первой
labels = plotpie['cat']
values = plotpie['revenue']
ax1.pie(values,labels = labels,autopct = '%1.1f%%') #plot first pie
ax1.set_title('Выручка по категориям')

# данные и описания для второй
labels = plotpie['cat']
values = plotpie['quantity']
ax2.pie(values,labels = labels,autopct = '%1.1f%%') #plot second pie
ax2.set_title('Количество по категориям')

# выводим их на экран
plt.show();

Видно, что выручка и количество товара распределены неравномерно.

Во-первых, 44% продаж приходится на растения, но приносят они лишь 16% выручки. Вероятно, растения для посадки закупаются не круглый год, поэтому важно заполнять ими складские помещения именно накануне пикового спроса.

Во-вторых, самая малочисленная категория "хранение и перевозка" приносит более 21% выручки. За ней следуют, одежда с обувью и хозтовары, каждая из которых отвечает за пятую часть выручки.

Наконец, кухонные и прочие товары приносят лишь 18% выручки, занимая 25% продаж. Возможно, магазину стоит отказаться от закупок товаров этих категорий в пользу более прибыльных.

Посмотрим, как распределены продажи по времени: сперва по месяцам, а далее по дням недели и часам

In [44]:
catsum = df.groupby(['ymonth','cat'])[['quantity']].sum()

# переименуем столбец
catsum.columns = ['total_quantity']


# строим столбчатую диаграмму 
fig = px.bar(catsum.reset_index().sort_values(by=['total_quantity'], ascending=False), # загружаем данные и заново их сортируем
             x='total_quantity', # указываем столбец с данными для оси X
             y='ymonth', # указываем столбец с данными для оси Y
             color='cat',
            )
# оформляем график
fig.update_layout(title='Продажи товаров по категориям',
                   xaxis_title='Количество в шт',
                   yaxis_title='Месяц',
                   yaxis={'categoryorder':'total ascending'})
fig.show() # выводим график

В поисках сезонности

Мы подтвердили нашу гипотезу, что продажи растений определяются сезонностью. Так, пик продаж приходится на весну, или апрель-май, когда начинается сезон посадок. Этот же факт отвечает на наше раннее наблюдение, что в апреле-мае наблюдаются пики продаж. Эта информация пригодится магазину для эффективного управления складскими помещениями.

Продажи одежды и обуви, наоборот, логично растут в в холодные осенние и зимние месяцы, падая к лету. То же самое происходит с кухонными и прочими товарами.

В продажах хозтоваров и ёмкостей не наблюдается ярко выраженной сезонности, однако они сокращаются к концу года. Сложно делать однозначный вывод о долгосрочном тренде на основании данных за октябрь 2018 и октябрь 2019 года, но по всех категориям товаров, кроме растений и ёмкостей, продажи падают к октябрю 2019 года в 1,5-4 раза. Это может свидетельствовать о низкой конкурентоспособности магазина во всех категориях, кроме растений.

Теперь построим хитмепы для анализа продаж в разрезе дней недели и часов

In [45]:
# строим пивот, от которого будем отрезать данные по категориям
heatmap_catd = (df.pivot_table(index=['cat','dow','hour'], values='quantity', aggfunc='sum')
              .reset_index()
              .sort_values('dow', ascending=True)
                  )
In [46]:
# приводим в табличный вид
plants_hm = (heatmap_catd.query('cat == "растения"')
                         .pivot('hour', 'dow', 'quantity')
            )

# строим хитмеп нужного размера 
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа растений в разрезе дней недели и часов')
display(sns.heatmap(plants_hm, annot=True));
<AxesSubplot:title={'center':'Продажа растений в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
In [47]:
# приводим в табличный вид
clothes_hm = (heatmap_catd.query('cat == "одежда и обувь"')
                         .pivot('hour', 'dow', 'quantity')
            )

# строим хитмеп нужного размера 
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа одежды и обуви в разрезе дней недели и часов')
display(sns.heatmap(clothes_hm, annot=True))
<AxesSubplot:title={'center':'Продажа одежды и обуви в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
In [48]:
# приводим в табличный вид
appliances_hm = (heatmap_catd.query('cat == "дом и быт"')
                         .pivot('hour', 'dow', 'quantity')
            )

# строим хитмеп нужного размера 
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа товаров для дома в разрезе дней недели и часов')
display(sns.heatmap(appliances_hm, annot=True))
<AxesSubplot:title={'center':'Продажа товаров для дома в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>
In [49]:
# приводим в табличный вид
storage_hm = (heatmap_catd.query('cat == "хранение и перевозка"')
                         .pivot('hour', 'dow', 'quantity')
            )

# строим хитмеп нужного размера 
fig, ax = plt.subplots(figsize=(5,5))
plt.title('Продажа ёмкостей в разрезе дней недели и часов')
display(sns.heatmap(storage_hm, annot=True))
<AxesSubplot:title={'center':'Продажа ёмкостей в разрезе дней недели и часов'}, xlabel='dow', ylabel='hour'>

Во всех случаях продажи практически отсутствуют в ночное время до 9 часов утра. Пик приходится на 9-15 часов, а растения активно покупают и после 15.

Кроме продаж ёмкостей, которые распределены более-менее равномерно по дням, основной объём продаж приходится на понедельник-среду.

Поскольку в дневные часы наблюдается основной поток заказов, можно было бы ввести скидки на заказы, совершаемые поздно вечером, чтобы сгладить нагрузку на операторов.

Посмотрим, можем ли мы отделить категории товаров, которые покупают вместе с другими товарами, от покупаемых отдельно.

Основной и дополнительный ассортимент

In [50]:
# сделаем пивот по номерам заказов и количеством уникальных товаров в каждом
orders_ops = (df.pivot_table(index='order_id', values='prod', aggfunc='nunique')
                      .reset_index()
                      .sort_values('prod', ascending=False)
                     ) 


# приведём кол-во к целочисленному типу данных
orders_ops['prod'] = orders_ops['prod'].astype('int')

# оставим только заказы, в которых покупают один уникальный товар
orders_ops = orders_ops['order_id'].loc[orders_ops['prod'] == 1]

# переведём серию в список
orders_ops = orders_ops.tolist()

orders_ops
Out[50]:
[71510,
 71538,
 71516,
 71934,
 71513,
 71530,
 71529,
 71936,
 71945,
 71938,
 71523,
 71939,
 71519,
 71937,
 71522,
 71526,
 71515,
 71525,
 71524,
 71514,
 71941,
 71552,
 71541,
 71565,
 71560,
 71915,
 71911,
 71909,
 71908,
 71561,
 71566,
 71922,
 71568,
 71572,
 71573,
 71907,
 71574,
 71578,
 71918,
 71559,
 71933,
 71551,
 71931,
 71546,
 71930,
 71547,
 71549,
 71550,
 71505,
 71924,
 71555,
 71556,
 71557,
 71558,
 71928,
 71926,
 71509,
 71948,
 71504,
 71391,
 71392,
 71393,
 71396,
 71967,
 71397,
 71398,
 71966,
 71965,
 71963,
 71961,
 71399,
 71400,
 71409,
 71411,
 71412,
 71972,
 71974,
 71502,
 71390,
 71352,
 71353,
 71354,
 71362,
 71363,
 71364,
 71365,
 71976,
 71368,
 71370,
 71375,
 71378,
 71385,
 71388,
 71389,
 71413,
 71414,
 71415,
 71959,
 71458,
 71460,
 71462,
 71463,
 71950,
 71466,
 71581,
 71467,
 71469,
 71470,
 71479,
 71484,
 71491,
 71498,
 71499,
 71457,
 71952,
 71456,
 71426,
 71957,
 71417,
 71956,
 71419,
 71422,
 71955,
 71954,
 71454,
 71428,
 71953,
 71432,
 71440,
 71441,
 71450,
 71580,
 71792,
 71903,
 71725,
 71713,
 71716,
 71717,
 71723,
 71724,
 71850,
 71849,
 71848,
 71860,
 71846,
 71844,
 71728,
 71837,
 71732,
 71835,
 71834,
 71708,
 71705,
 71703,
 71701,
 71855,
 71685,
 71687,
 71689,
 71690,
 71691,
 71693,
 71694,
 71695,
 71696,
 71853,
 71852,
 71697,
 71698,
 71700,
 71733,
 71349,
 71833,
 71805,
 71803,
 71761,
 71764,
 71802,
 71765,
 71767,
 71801,
 71770,
 71780,
 71785,
 71786,
 71800,
 71788,
 71799,
 71789,
 71760,
 71807,
 71831,
 71808,
 71736,
 71738,
 71829,
 71828,
 71744,
 71750,
 71751,
 71753,
 71755,
 71756,
 71758,
 71817,
 71816,
 71759,
 71810,
 71858,
 71682,
 71902,
 71617,
 71604,
 71605,
 71606,
 71612,
 71614,
 71615,
 71894,
 71620,
 71681,
 71621,
 71623,
 71630,
 71633,
 71634,
 71893,
 71635,
 71603,
 71895,
 71898,
 71601,
 71587,
 71901,
 71589,
 71900,
 71591,
 71592,
 71798,
 71593,
 71594,
 71595,
 71899,
 71597,
 71598,
 71599,
 71600,
 71636,
 71892,
 71638,
 71662,
 71664,
 71876,
 71665,
 71874,
 71667,
 71670,
 71865,
 71673,
 71674,
 71863,
 71677,
 71678,
 71862,
 71679,
 71680,
 71877,
 71878,
 71889,
 71661,
 71639,
 71886,
 71640,
 71642,
 71643,
 71644,
 71645,
 71646,
 71647,
 71885,
 71657,
 71882,
 71881,
 71880,
 71879,
 71351,
 12624,
 71347,
 70861,
 70905,
 70908,
 70909,
 70913,
 70915,
 70916,
 70919,
 70921,
 70922,
 70924,
 70925,
 70931,
 70932,
 70933,
 70934,
 70938,
 70939,
 70904,
 70899,
 70897,
 70876,
 70863,
 70864,
 70866,
 70869,
 70870,
 70872,
 70874,
 70877,
 70896,
 70878,
 70881,
 70883,
 70885,
 70888,
 70891,
 70895,
 70943,
 70947,
 70949,
 71012,
 70999,
 71000,
 71004,
 71007,
 71008,
 71009,
 71010,
 71014,
 70988,
 71015,
 71016,
 71017,
 71018,
 71023,
 71027,
 71028,
 70992,
 70986,
 70951,
 70965,
 70953,
 70956,
 70959,
 70960,
 70961,
 70962,
 70964,
 70968,
 70983,
 70973,
 70974,
 70976,
 70978,
 70980,
 70981,
 70982,
 70862,
 70860,
 71030,
 70859,
 70742,
 70743,
 70746,
 70747,
 70748,
 70749,
 70751,
 70755,
 70757,
 70758,
 70762,
 70764,
 70765,
 70766,
 70769,
 70770,
 70772,
 70741,
 70740,
 70736,
 70710,
 70693,
 70694,
 70697,
 70702,
 70704,
 70708,
 70709,
 70712,
 70734,
 70713,
 70718,
 70721,
 70723,
 70725,
 70727,
 70728,
 70773,
 70774,
 70776,
 70843,
 70832,
 70833,
 70834,
 70837,
 70839,
 70840,
 70842,
 70845,
 70828,
 70846,
 70847,
 70851,
 70853,
 70854,
 70856,
 70857,
 70831,
 70822,
 70782,
 70800,
 70785,
 70788,
 70793,
 70794,
 70796,
 70797,
 70799,
 70801,
 70821,
 70803,
 70807,
 70809,
 70812,
 70814,
 70818,
 70819,
 71029,
 71031,
 71345,
 71192,
 71230,
 71232,
 71233,
 71234,
 71239,
 71240,
 71242,
 71244,
 71245,
 71247,
 71248,
 71249,
 71251,
 71253,
 71254,
 71255,
 71256,
 71227,
 71225,
 71223,
 71207,
 71195,
 71197,
 71198,
 71200,
 71202,
 71205,
 71206,
 71209,
 71222,
 71211,
 71213,
 71214,
 71217,
 71218,
 71219,
 71220,
 71257,
 71258,
 71261,
 71328,
 71309,
 71310,
 71311,
 71319,
 71320,
 71322,
 71324,
 71329,
 71306,
 71330,
 71331,
 71333,
 71335,
 71336,
 71341,
 71344,
 71307,
 71302,
 71262,
 71279,
 71264,
 71265,
 71266,
 71267,
 71271,
 71272,
 71275,
 71284,
 71301,
 71287,
 71288,
 71289,
 71291,
 71294,
 71299,
 71300,
 71193,
 71191,
 71032,
 71188,
 71074,
 71075,
 71076,
 71077,
 71080,
 71083,
 71085,
 71088,
 71089,
 71091,
 71093,
 71094,
 71098,
 71101,
 71103,
 71107,
 71108,
 71071,
 71065,
 71063,
 71043,
 71033,
 71034,
 71035,
 71038,
 71039,
 71041,
 71042,
 71044,
 71057,
 71045,
 71046,
 71048,
 71049,
 71050,
 71053,
 71055,
 71111,
 71113,
 71121,
 71170,
 71156,
 71157,
 71159,
 71160,
 71162,
 71163,
 71164,
 71171,
 71154,
 71174,
 71980,
 71175,
 71176,
 71177,
 71178,
 71186,
 71155,
 71153,
 71122,
 71133,
 71124,
 71125,
 71126,
 71127,
 71128,
 71130,
 71131,
 71136,
 71152,
 71138,
 71139,
 71140,
 71141,
 71142,
 71148,
 71149,
 71979,
 72220,
 71984,
 72788,
 72773,
 72776,
 72779,
 72780,
 72781,
 72786,
 72787,
 72791,
 72727,
 72792,
 72793,
 72794,
 72795,
 72796,
 72797,
 72799,
 72772,
 72771,
 72770,
 72769,
 72732,
 72734,
 72741,
 72742,
 72744,
 72745,
 72746,
 72747,
 72748,
 72753,
 72759,
 72764,
 72765,
 72766,
 72767,
 72800,
 72801,
 72802,
 72836,
 72844,
 72847,
 72848,
 72849,
 72852,
 72854,
 72858,
 72859,
 72861,
 72862,
 72863,
 72865,
 72866,
 72867,
 72868,
 72843,
 72834,
 72803,
 72833,
 72805,
 72806,
 72810,
 72814,
 72817,
 72818,
 72819,
 72820,
 72821,
 72824,
 72826,
 72827,
 72828,
 72829,
 72831,
 72729,
 72726,
 72870,
 72617,
 72608,
 72609,
 72611,
 72613,
 72614,
 72615,
 72616,
 72618,
 72722,
 72621,
 72625,
 72627,
 72628,
 72634,
 72635,
 72637,
 72606,
 72605,
 72600,
 72598,
 72568,
 72571,
 72574,
 72577,
 72578,
 72581,
 72582,
 72583,
 72585,
 72586,
 72587,
 72590,
 72592,
 72593,
 72595,
 72638,
 72639,
 72641,
 72686,
 72690,
 72691,
 72695,
 72696,
 72697,
 72704,
 72707,
 72710,
 72713,
 72714,
 72715,
 72717,
 72718,
 72719,
 72720,
 72689,
 72684,
 72648,
 72683,
 72649,
 72651,
 72652,
 72653,
 72657,
 72658,
 72660,
 72667,
 72673,
 72674,
 72675,
 72678,
 72679,
 72681,
 72682,
 72869,
 72871,
 72566,
 73060,
 73046,
 73047,
 73050,
 73051,
 73052,
 73055,
 73057,
 73063,
 73000,
 73066,
 73068,
 73069,
 73071,
 73072,
 73073,
 73074,
 73044,
 73041,
 73040,
 73039,
 73003,
 73008,
 73014,
 73015,
 73016,
 73017,
 73025,
 73027,
 73030,
 73031,
 73032,
 73034,
 73036,
 73037,
 73038,
 73077,
 73082,
 73083,
 73136,
 73138,
 73140,
 73141,
 73142,
 73143,
 73144,
 73146,
 73147,
 73148,
 73151,
 73154,
 73155,
 73156,
 73158,
 73162,
 73137,
 73131,
 73084,
 73130,
 73086,
 73092,
 73093,
 73094,
 73095,
 73097,
 73101,
 73104,
 73105,
 73108,
 73112,
 73115,
 73123,
 73126,
 73129,
 73002,
 72999,
 72872,
 72922,
 72912,
 72913,
 72914,
 72915,
 72917,
 72918,
 72919,
 72923,
 72998,
 72924,
 72925,
 72926,
 72927,
 72929,
 72930,
 72931,
 72909,
 72907,
 72905,
 72904,
 72873,
 72874,
 72876,
 72881,
 72883,
 72884,
 72889,
 72890,
 72892,
 72893,
 72896,
 72899,
 72900,
 72901,
 72903,
 72934,
 72936,
 72937,
 72974,
 72976,
 72977,
 72978,
 72981,
 72983,
 72984,
 72986,
 72987,
 72988,
 72991,
 72992,
 72993,
 72995,
 72996,
 72997,
 72975,
 72973,
 72938,
 72969,
 72940,
 72942,
 72944,
 72945,
 72946,
 72947,
 72949,
 72951,
 72952,
 72953,
 72954,
 72958,
 72959,
 72965,
 72967,
 72567,
 72564,
 71985,
 72170,
 72159,
 72161,
 72162,
 72164,
 72165,
 72168,
 72169,
 72173,
 72123,
 72177,
 72179,
 72180,
 72183,
 72189,
 72190,
 72193,
 72153,
 72152,
 72151,
 72150,
 72125,
 72126,
 72127,
 72128,
 72129,
 72130,
 72132,
 72133,
 72134,
 72135,
 72139,
 72140,
 72141,
 72142,
 72149,
 72194,
 72195,
 72196,
 70690,
 72225,
 72227,
 72228,
 72229,
 72230,
 72231,
 72232,
 72233,
 72235,
 72237,
 72240,
 72249,
 72250,
 72252,
 72256,
 72223,
 72219,
 72197,
 72218,
 72198,
 72200,
 72201,
 72202,
 72204,
 72205,
 72206,
 72208,
 72209,
 72210,
 72211,
 72212,
 72214,
 72216,
 72217,
 72124,
 72119,
 72262,
 72035,
 72022,
 72026,
 ...]
In [51]:
# добавим столбец в датасет, отвечающий на вопрос, один ли уникальный товар содержится в заказе или нет
df['sole_order'] = [x in orders_ops for x in df['order_id']]

df['sole_order']
Out[51]:
0       False
1       False
2       False
3       False
4       False
        ...  
4778     True
4779     True
4780     True
4781     True
4782     True
Name: sole_order, Length: 4783, dtype: bool
In [52]:
# Построим диаграммы распределения продажи товаров по категориям (заказы с одним уникальным товаром)
display(df.query('sole_order == True')
              .pivot_table(index='cat', values='quantity', aggfunc='sum')
              .reset_index()
              .sort_values('quantity', ascending=False)
              .plot(x='cat', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Если в заказе один товар, то какой категории?')
plt.show()
<AxesSubplot:xlabel='cat'>
In [53]:
# Построим диаграммы распределения продажи товаров по категориям (заказы с несколькими уникальными товарами)
display(df.query('sole_order == False')
              .pivot_table(index='cat', values='quantity', aggfunc='sum')
              .reset_index()
              .sort_values('quantity', ascending=False)
              .head(15)
              .plot(x='cat', y='quantity', kind='bar')); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Если в заказе несколько товаров, то каких категорий?')
plt.show()
<AxesSubplot:xlabel='cat'>

Итак, мы имеем следующее соотношение основного и дополнительного ассортимента по каждой категории:

1. Растения: 52% основного и 48% дополнительного
2. Дом и быт: 84% и 16%
3. Прочее: 82% и 18%
4. Кухня: 82% и 18%
5. Одежда и обувь: 85% и 15%
6. Хранение и перевозка: 91% и 9%

Посмотрим, какие товары входят в основной и допассортимент.

In [54]:
# посмотрим и сравним топ-3 товара каждой категории основного и дополнительного ассортимента
for i in df['cat'].unique(): # перебираем все категории
    cats = (df.query('cat == @i')
        .groupby(['sole_order','cat','prod'])[['quantity']].sum()
        .sort_values(by=['cat','quantity'], ascending=False)
        .reset_index()
           )
    for k in df['sole_order'].unique(): # перебираем основной и допассортимент
        display(cats.query('sole_order == @k').head(3)) # выводим топ-3 товара по количеству продаж
sole_order cat prod quantity
3 False растения Пеларгония зональная 239
5 False растения Пеларгония розебудная 156
6 False растения Рассада зелени 149
sole_order cat prod quantity
0 True растения Искусственный цветок 323
1 True растения Цветок искусственный 298
2 True растения Пеларгония зональная 249
sole_order cat prod quantity
10 False одежда и обувь Вешалка деревянная 20
11 False одежда и обувь Вешалка для 19
12 False одежда и обувь Плечики пластмассовые 19
sole_order cat prod quantity
0 True одежда и обувь Сушилка для 278
1 True одежда и обувь Гладильная доска 125
2 True одежда и обувь Вешалка для 81
sole_order cat prod quantity
5 False дом и быт Щетка для 52
7 False дом и быт Щетка-утюжок с 50
15 False дом и быт Набор вешалок 28
sole_order cat prod quantity
0 True дом и быт Ёрш унитазный 103
1 True дом и быт Коврик придверный 98
2 True дом и быт Щетка-сметка 4-х 90
sole_order cat prod quantity
10 False хранение и перевозка Банка стеклянная 7
15 False хранение и перевозка Короб стеллажный 4
17 False хранение и перевозка Ящик для 3
sole_order cat prod quantity
0 True хранение и перевозка Сумка-тележка хозяйственная 111
1 True хранение и перевозка Сумка-тележка 2-х 92
2 True хранение и перевозка Тележка багажная 65
sole_order cat prod quantity
5 False кухонные принадлежности Нож кухонный 33
6 False кухонные принадлежности Кружка НОРДИК 30
12 False кухонные принадлежности Тарелка суповая 13
sole_order cat prod quantity
0 True кухонные принадлежности Таз пластмассовый 121
1 True кухонные принадлежности Тарелка обеденная 119
2 True кухонные принадлежности Тарелка десертная 80
sole_order cat prod quantity
8 False прочее Муляж Апельсин 29
10 False прочее Муляж Яблоко 25
14 False прочее Муляж Красное 20
sole_order cat prod quantity
0 True прочее Муляж Яблоко 124
1 True прочее Муляж Банан 105
2 True прочее Муляж Лимон 91

Различия в популярных товарах разных ассортиметов следующие:

  1. Растения:
  • основной: искусственные цветы и пеларгония
  • доп: пеларгония и рассада
  1. Дом и быт:
  • ёршики, коврики, щётки
  • щётки и вешалки
  1. Прочее: там и там муляжи, но разных фруктов
  2. Кухня:
  • тазы и тарелки
  • ножи, кружки, тарелки
  1. Одежда и обувь:
  • сушилки, гладильные доски и вешалки
  • вешалки и плечики
  1. Хранение и перевозка:
  • сумки и тележки
  • банки, коробы, ящики

Таким образом, зачастую ассортименты пересекаются, но по количеству можно выделить следующих лидеров основного ассортимента:

  1. Искусственные цветы
  2. Сушилки и гладильные доски
  3. Сумки и тележки
  4. Тазы
In [55]:
# Построим диаграммы частоты категории товаров в заказах с несколькими уникальными товарами
display(df.query('sole_order == False')
              .pivot_table(index='order_id', values='cat', aggfunc='nunique')
              .reset_index()
              .sort_values('cat', ascending=False)
              .hist('cat',bins=6)
       ); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Заказы в шт')
plt.title('В 200 заказах встречаются товары только одной категории')
plt.show()
array([[<AxesSubplot:title={'center':'cat'}>]], dtype=object)
In [56]:
# Построим диаграммы частоты категории товаров в заказах с несколькими уникальными товарами
display(df.query('sole_order == False')
              .pivot_table(index='cat', values='order_id', aggfunc='nunique')
              .reset_index()
              .sort_values('order_id', ascending=False)
              .plot(x='cat', y='order_id',kind='bar')
       ); # строим диаграмму
# называем оси и заголовок
plt.xlabel('Категория')
plt.ylabel('Товары в шт')
plt.title('Из 277 таких заказов растения есть более чем в 200')
plt.show()

# сколько всего таких заказов?
display(df.query('sole_order == False')['order_id'].nunique())
<AxesSubplot:xlabel='cat'>
277

Итак, мы видим, что товары всех категорий, исключая растения, в 5 случаях из 6 (или чаще) покупаются отдельно. Растения покупают отдельно в 40% процентах случаев.

Однако могут быть ситуации, когда в заказе есть несколько уникальных товаров, но все они принадлежат одной категории. Мы выяснили, что таких заказов более двухсот. При этом из 277 заказов, в которых присутствует 2 и более уникальных товара, в 200 есть растения.

Таким образом, хотя 60% растений покупаются вместе с другими товарами, эти другие товары в основном тоже растения.

Гипотезы о равенстве выручки и количества между основным и допассортиментом¶

Критерий вывода о значимости/незначимости различий: Непараметрический тест Уилкоксона-Манна-Уитни

Уровень значимости: 0,05

Нулевая гипотеза: В средней выручке между товарами основного и допассортимента по очищенным данным отсутствуют статистически значимые различия

Альтернативная гипотеза: Различия в средней выручке между товарами основного и допассортимента по очищенным данным статистически значимы

In [57]:
print('p-value:', "{0:.3f}".format(stats.mannwhitneyu(df[df['sole_order']== True]['revenue'], 
                                          df[df['sole_order']== False]['revenue'])[1]))
print('относительное различие средней выручки','{0:.3f}'.format(df[df['sole_order']== True]['revenue'].mean()/df[df['sole_order']== False]['revenue'].mean()-1)) 
p-value: 0.000
относительное различие средней выручки 3.764

Критерий вывода о значимости/незначимости различий: Непараметрический тест Уилкоксона-Манна-Уитни

Уровень значимости: 0,05

Нулевая гипотеза: В среднем количестве между товарами основного и допассортимента по очищенным данным отсутствуют статистически значимые различия

Альтернативная гипотеза: Различия в среднем количестве между товарами основного и допассортимента по очищенным данным статистически значимы

In [58]:
print('p-value:', "{0:.3f}".format(stats.mannwhitneyu(df[df['sole_order']== True]['quantity'], 
                                          df[df['sole_order']== False]['quantity'])[1]))
print('относительное различие среднего количества','{0:.3f}'.format(df[df['sole_order']== True]['quantity'].mean()/df[df['sole_order']== False]['quantity'].mean()-1)) 
p-value: 0.000
относительное различие среднего количества 0.597

В обоих случаях p-value меньше уровня значимости, поэтому принимаем гипотезы об отсутствии различий как в средней выручке, так и в среднем количестве проданных товаров основного и допассортимента.

5. Выводы и рекомендации¶

Проанализировав полученные данные, мы пришли к следующим выводам:

1. Данные по продажам охватывают 1 год и 1 месяц: с октября 2018 по октябрь 2019 года.
2. Мы обнаружили пропуски в нумерации заказов - вопрос к техподдержке о её адекватности.
3. Это розничный магазин, торгующий товарами преимущественно до 1 000 у.е.
4. Полных дубликатов не обнаружено, но есть как дублирующиеся в разное время заказы, так и ошибочные пары покупатель-заказ. Они заняли около 30% датасета.
5. Оптовые продажи составили 4,5% операций и треть (1,3 млн у.е.) выручки, в том числе единичная закупка 1000 единиц товара (вероятно, ошибочная запись).
6. Розничные продажи составили 2,8 млн у.е. Помесячный тренд говорит о постепенном сокращении выручки как в монетарном, так и в физическом выражении. Наиболее крупные по объёму закупки наблюдаются в апреле и мае, в пик продаж растений.
7. Структура продаж неоднородная:

    Во-первых, более половины продаж приходится на растения, но приночят они лишь 17% выручки. Вероятно, растения для посадки закупаются не круглый год, поэтому важно заполнять ими складские помещения именно накануне пикового спроса.

    Во-вторых, самая малочисленная категория "хранение и перевозка" приносит больше всего (четверть) выручки. За ней следуют, одежда с обувью и хозтовары, каждая из которых отвечает за пятую часть выручки.

    Наконец, кухонные и прочие товары приносят лишь 15% выручки, занимая 17% продаж. Возможно, магазину стоит отказаться от закупок товаров этих категорий в пользу более прибыльных.
8. Сезонность наблюдается в продажах растений (апрель-май) и одежды и обуви (осень-зима). По дням недели распределение практически равномерное, основной объём продаж приходится на первую половину дня, или с 9 до 15 часов (до 18 для растений).
9. 85% товаров всех категорий, исключая растения, продаются отдельно. Растения продаются как отдельно (40%), так и вместе с другими товарами. Однако большинство этих других товаров - другие растения.

Рекомендации:

1. Подготовить для операторов заказов единую инструкцию по занесению данных в базу, чтобы не было пропусков и корявых заказов.
2. Возможно, стоит придумать выгодные предложения для оптовых покупателей: их немного, но треть выручки приходится на них.
3. Внести корректировки в систему закупок и управления складами. Растения занимают больше всего места, но продаются практически только весной. Соответственно, осенью-зимой можно было бы освободить больше места для других категорий товаров: одежды и обуви, нужные в холод, и ёмкостей, которые приносят больше выручки.
4. Следует сократить или отказаться от закупки кухонных и прочих товаров в пользу более прибыльных.
5. Подумать над системой скидок при заказе товаров вечером, чтобы разгрузить операторов.